React Native -- 布局位置随输入框变化的问题处理

前言

在之前项目的开发过程中,下图这样的需求很是常见:
1.gif
当键盘弹起时,某个布局正好在键盘之上,当键盘消失时,这个布局又回到页面最底部。

今天来讨论的就是这个功能我用过的方法及踩过的坑。

方案1

首先我们观察到下面的一块布局(以下简称 “A” 布局)是随着键盘的弹起和消失来改变位置的,并且距离屏幕的底部正好是键盘的高度。
所以,第一个思路,设置 A 布局的位置方式为绝对定位,并且是计算距离父布局底部的位置为键盘的高度。
代码中定义一个展示键盘高度的变量 keyboardHeight ,然后设置键盘显示与消失的事件监听,并给 keyboardHeight 变量赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
componentDidMount(){
this.keyboardDidShow = Keyboard.addListener('keyboardDidShow', this.keyboardShow.bind(this));
this.keyboardDidHide = Keyboard.addListener('keyboardDidHide', this.keyboardHide.bind(this));
}

keyboardShow(e){
this.setState({
keyboardHeight:e.endCoordinates.height
});
};
keyboardHide(){
this.setState({
keyboardHeight:0
});
}

然后在 render 方法中设置 A 布局的样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
render() {
return (
<View style = {{flex:1}}>
<TextInput
style={{height: 140, borderColor: '#999', borderWidth: 1,margin:15}}
onChangeText={(text) => this.setState({text})}
placeholder={'随便说点什么吧'}
textAlignVertical = {'top'}
/>

<View style = {[styles.bottomView,{bottom:this.state.keyboardHeight}]}>
<TouchableOpacity onPress = {()=>{alert(1)}}>
<Image style = {{width:26,height:26,marginLeft:15}} resizeMode ={'contain'}source = {require('../img/keyBoard.png')}/>
</TouchableOpacity>
<TouchableOpacity onPress = {()=>{alert(1)}}>
<Image style = {{width:26,height:26,marginRight:15}} resizeMode ={'contain'}source = {require('../img/emoji.png')}/>
</TouchableOpacity>
</View>
</View>

);
}

const styles = StyleSheet.create({

bottomView:{
position:'absolute',
flexDirection:'row',
height:48,width:'100%',
backgroundColor:'#DD4F43',
justifyContent:'space-between',
alignItems:'center'
}
});

然后,我们来看一下效果:
2.gif

咦,好像翻车了。
可以看到当键盘没有弹出的时候是好的,但是当键盘弹出后 A 布局就不见了。那这又有两种情况,一种是 A 布局在正确位置的上方,超出屏幕了,一种是在正确位置的下方,被键盘遮挡没弹起来。
那我们来将 A 布局的位置改为 keyboardHeight -150

看下效果:
3.gif
嗯,A 布局出现了,并且是在键盘的上方。由此可见我们的第一个猜想是对的。
那我们不妨再大胆做个猜想,A 布局偏离正确位置的高度会不会就是一个键盘的高度呢?如果是这样的话,那我们 A 布局的 bottom 就应该为 keyboardHeight - keyboardHeight 即为 0!

改下代码,然后来看下效果
4.gif
貌似完美! 也就是在 android 上也不需要去计算键盘的高度,布局直接在键盘之上?这个原理我还不清楚,但感觉和输入框与软键盘之间的恩怨有联系。
然后我们再看下在 ios 上的表现,嗯,这里没有测试机,就不贴图了。但事实证明会有个问题,在 ios 上如果这样设置当键盘显示的时候,A 布局不会弹起来,而是还在屏幕的底部。所以,ios 还是要计算键盘的高度。

所以,最后适配如下:

1
2
3
4
5
6
7
8
<View style = {[styles.bottomView,{bottom:Platform.OS == 'ios'? this.state.keyboardHeight:0}]}>
<TouchableOpacity onPress = {()=>{alert(1)}}>
<Image style = {{width:26,height:26,marginLeft:15}} resizeMode ={'contain'}source = {require('../img/keyBoard.png')}/>
</TouchableOpacity>
<TouchableOpacity onPress = {()=>{alert(1)}}>
<Image style = {{width:26,height:26,marginRight:15}} resizeMode ={'contain'}source = {require('../img/emoji.png')}/>
</TouchableOpacity>
</View>

在这种方案实行了俩星期后,测试就提出来严重的兼容性问题:

  • 在某些 android 手机上(如魅蓝),键盘弹起时,A 布局会距离键盘上方大约一个键盘的高度,即弹的太上了
  • 在某些 android 手机 或者 iPhoneX 上,键盘弹起时 A 布局弹不出来

这两个问题都是偶现的,但在某些机型上的偶现率达 50%
所以,看来方案一是有问题。
但问题究竟出在哪里,我没弄明白,但敢肯定还是和输入框与软键盘的适配有关,曾经尝试设置 activity 的 softnput 参数,未果。

方案2

那我们就来换种思路,方案一我们是采用基于 bottom 的定位。但也许由于软键盘与输入框的特殊关系,这个 bottom 的值在不同机型上表现不同。那我们能否改为基于 top 定位呢。
分析一下,如果是基于 top 定位的情况,值应该怎么算,来看一下分析图:
5.png

  • 当键盘没有弹起的时候,A 布局距离父布局的顶部的距离是 h- bottomHeight,即父布局高度减去 A 布局自身高度。
  • 当键盘弹起时,A 布局距离父布局的顶部的距离是 h- bottomHeight-keyboardHeight,即多减掉键盘的高度。

    所以最终计算方式为:父布局的高度 - A 布局自身的高度 - 键盘的高度

    代码中,我们首先需要计算一下父布局的高度,通过 onLayout 方法计算出。然后设置 A 布局的 top 属性值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
_onLayout(event) {
//获取根View的宽高,以及左上角的坐标值
this.setState({
layoutHeight : 1
})
}
render() {
return (
<View style = {{flex:1}} onLayout={()=>{this._onLayout}}>
<TextInput
style={{height: 140, borderColor: '#999', borderWidth: 1,margin:15}}
onChangeText={(text) => this.setState({text})}
placeholder={'随便说点什么吧'}
textAlignVertical = {'top'}
/>

<View style = {[styles.bottomView,{top:520-48-this.state.keyboardHeight}]}>
<TouchableOpacity onPress = {()=>{alert(1)}}>
<Image style = {{width:26,height:26,marginLeft:15}} resizeMode ={'contain'}source = {require('../img/keyBoard.png')}/>
</TouchableOpacity>
<TouchableOpacity onPress = {()=>{alert(1)}}>
<Image style = {{width:26,height:26,marginRight:15}} resizeMode ={'contain'}source = {require('../img/emoji.png')}/>
</TouchableOpacity>
</View>
</View>

);
}

刷新一下,看一下效果:
6.gif

貌似没什么毛病,通过在其他机型和 ios 上测试,目前没有发现问题。

所以第二种方案可行。

总结

以后遇到这种需求,最好要避免根据 bottom 来定位,可以换种思路,根据 top 来定位。


------------- 本文结束 感谢您的阅读 -------------